本文作为《Game Programming Pattern》(游戏编程原理)的摘要总结,记录为期一个月的阅读感想与心得体会。
撰写本文时,希望可以尽量遵循star法则,此后的文章同样以这四个要点作为大纲。
situation(情境)
task(任务)
action(行为)
result(结果)
本文代码尽量以unity c#进行演示(最近看的都是cpp代码,时常有被带偏的情况,本人实在太菜菜子了)
观察者模式
在对象间定义一种一对多的依赖关系,以便当某对象的状态改变时,与他存在依赖关系的所有对象都能收到通知并自动进行更新。
观察者模式是最为人所知的设计模式之一,使用最为广泛。我们可以用它来实现成就系统(成就要要检测敌人死亡个数,被观察者就是敌人死亡,观察者就是成就系统。)。
在c#中观察者模式被集成到了语言层面(event)
situation
将一个系统分割成一个一些类相互协作的类有一个不好的副作用,那就是需要维护相关对象间的一致性。
task
我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。
action
我们先写一个抽象观察者类
public abstract class Observe {
//得到通知后的响应
public abstract void onNotify(const Entity entity,Event event);
}
任何实现这个接口的具体类都会成为一个观察者。我们针对这个抽象类进行编程。
我们来完成一个成就系统吧
public class Achivement:Observe
{
public override void onNotify(const Entity entity,Event event)
{
switch(event)
{
case EVENT_1:
{
...();
break
}
...
}
}
成就系统作为观察者,能在被观察者更新时收到通知,任何进行更新。
我们接下来编写被观察者Subject(这里我们创建一个观察者集合,使用委托来写观察者模式可能会更方便些,但使用集合可能更通用)。
public abstract class Subject
{
//观察者集合
protected List<IObserver> mObserverList;
public ISubject()
{
mObserverList = new List<IObserver>();
}
// 添加观察者
public void AddObserber(IObserver observer)
{
//让我们检查一下吧
if (mObserverList.Contains(observer) == false && observer != null)
{
mObserverList.Add(observer);
return;
}
Debug.Log("报错");
}
// 移除观察者
public void RemoveObserber(IObserver observer)
{
if (mObserverList.Contains(observer))
{
mObserverList.Remove(observer);
return;
}
Debug.Log("报错");
}
// 通知所有观察者更新
protect void NotifyObserver()
{
foreach (IObserver item in mObserverList)
{
item.onNotify();
}
}
}
我们的被观察者类完成了两个职责。一是它维护一个观察者列表,这些观察者随时等待通知。二是被观察者对象的NotifyObserver方法能够发送通知,调动观察者内的onNotify进行更新,同时我们也保护了NotifyObserver方法。
我们接下来只要使我们所需要的系统和Subject挂钩,就能将其登记到我们的成就系统上。我们可以调用NotifyObserver来发送通知,而任何地方都能调用添加和移除观察者的方法。
result
我们成功降低了耦合度,同时也满足开闭原则。
我们的c#有垃圾回收机制,所以可以不需要管理动态内存分配,一切都很合理。
与此同时我们得注意,观察者模式是同步的,这意味着所有的观察者得到通知后才能继续工作,而任何一个观察者对象都可能阻塞被观察者,我们需要更小心的处理线程。
我们还得注意及时清理例如死去的敌人那样失效的观察者。
观察者只能被动的获取数据,并不能主动的获取到想要的数据。因此它适合一些不相关的模块之间的通信问题,而不适合按个紧凑的模块。
原型模式
使用特定原型实例来创建特定种类的对象,并通过拷贝原型来创造新的对象。
situation
假设我们为游戏里面用一个抽象Monster类开始进行编程,那么可能遇到很多种继承它的敌人,人类、动物、龙,恶魔,各种等等。如果我们想为每个敌人做一个生成器父类Spawner,也会有与monster对应数量的子类。
这样就会产生类的数量很多,而且这些类的功能是重复的。这样造成了大量的冗余。(除非你的薪资以代码行数来计算)
task
我需要代码能够更加简洁。
action
为了解决最重要的冗余问题,我们先设计最开始的Monster,它有一个抽象方法clone。
public class Monster {
public string MonsterName;
public int attack;
public int defense;
public string weapon;
public abstract Monster clone();
}
C#数据类型大体分为值类型(valuetype)与引用类型 (referencetype)。
对于引用型变量而言,复制时,其实只是复制了其引用。复制引用的方式叫浅复制,
如果你想浅复制clone函数为:
public Monster clone()
{
return this;
}
它返回它本身的引用
若要深复制,就Instantiate一个新的:
public GameObject clone()
{
return Instantiate(this.gameObject) as GameObject;
}
回归正题!让我们来看看子类吧
public class Dragon : Monster
{
public Dragon()
{
MonsterName = "Dragon";
attack = 8;
defense = 15;
weapon = "tooth";
}
public override Monster clone()
{
return new Dragon(a,b,c,d);
}
}
只要让所有的这些子类都实现这些接口,我们就不需要单独再为每一个monster子类定义一个他们的spawner类了。
我们只需要定义一个spawner类:
public class Spawner
Monster mPrototype;
public Spawner(Monster Prototype)
{
...
}
public void setPrototype(Monster prototype)
{
...
}
public GameObject createMonster()
{
return prototype.clone();
}
}
使用时只需要:
spawner.setPrototype(A);
spawner.createMonster();
spawner.setPrototype(B);
spawner.createMonster();
我们也可以通过实例化spawner来创造各种各样的生成器。
这样就能创造出风龙水龙土龙大龙的生成器啦。
result
实际上我们并没有节省多少代码量,因为各种各样的怪物的clone方法可能也不一定相同。
为每种怪物编写clone方法和实现单独的生成器其实差不了多少,但起码我们让自己的逻辑更加清晰了,代码也变得更加优雅。
我们需要注意深拷贝和浅拷贝的问题,否则编写出来的clone方法可能是有问题的。
如果我们想把代码变成实在的数据也是很方便的,就比如Monster里的属性都是相同的,我们只需要一个Monster类和一个存了各种敌人的数据的文件就行了。
我们通过序列化和反序列化对原型进行数据建模,再将其传递给spawner。这在数据量大的游戏中尤为明显,能省去大量代码。使用JSON,XML,二进制等等都可以,这样也便于实现游戏的存档读档功能。我更喜欢json,因为它更易读。
2021.8.16–19:04完成